package com.sunlands.zlcx.usercenter.service;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.sunlands.zlcx.usercenter.client.DeskMatePushService;
import com.sunlands.zlcx.usercenter.client.GroupInfoService;
import com.sunlands.zlcx.usercenter.client.LiveVideoService;
import com.sunlands.zlcx.usercenter.client.UserFriendsService;
import com.sunlands.zlcx.usercenter.client.bean.PushDTO;
import com.sunlands.zlcx.usercenter.config.RedisConfig;
import com.sunlands.zlcx.usercenter.constant.*;
import com.sunlands.zlcx.usercenter.domain.*;
import com.sunlands.zlcx.usercenter.domain.log.UserActiveLogDO;
import com.sunlands.zlcx.usercenter.exception.CheckParamException;
import com.sunlands.zlcx.usercenter.exception.CheckRunException;
import com.sunlands.zlcx.usercenter.exception.SendExceptionUtil;
import com.sunlands.zlcx.usercenter.exception.UserLoginLimitException;
import com.sunlands.zlcx.usercenter.processor.online.chain.OfflineProcessorChain;
import com.sunlands.zlcx.usercenter.processor.online.chain.OnlineProcessorChain;
import com.sunlands.zlcx.usercenter.repository.*;
import com.sunlands.zlcx.usercenter.util.*;
import com.sunlands.zlcx.usercenter.vo.TimeLearnRoomSeatsVO;
import com.sunlands.zlcx.usercenter.vo.UsersVO;
import com.sunlands.zlcx.usercenter.vo.feign.TzUserBlacklist;
import com.sunlands.zlcx.usercenter.vo.feign.UserFriendInfo;
import com.sunlands.zlcx.usercenter.vo.feign.UsersGroupVO;
import com.sunlands.zlcx.usercenter.vo.response.BusinessResult;
import com.sunlands.zlcx.usercenter.vo.response.PageResultVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import javax.annotation.Resource;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.sunlands.zlcx.usercenter.config.RedisConfig.CacheKeys.*;
import static com.sunlands.zlcx.usercenter.constant.LearningStatus.NO_TIMING;
import static com.sunlands.zlcx.usercenter.controller.UsersController.LOGIN_LIMIT_STATUS;
import static com.sunlands.zlcx.usercenter.domain.UsersDO.FIELD_ID;
import static com.sunlands.zlcx.usercenter.domain.UsersDO.FIELD_ONLINE;
import static com.sunlands.zlcx.usercenter.domain.UsersDO.StatusEnums.LEARN_TIME_ROOM_LEARNING;
import static com.sunlands.zlcx.usercenter.domain.UsersDO.StatusEnums.LIVE_LEARNING;
import static com.sunlands.zlcx.usercenter.domain.UsersLearnTimingDO.StatusEnum.start;
import static com.sunlands.zlcx.usercenter.domain.UsersStudyInfoDO.TypeEnums.TIME_LEARN_ROOMING;

/**
 * @author anjl
 * @date 1:33 PM 2019/4/15
 * description: 用户处理业务相关的userserice
 */
@Service
@Slf4j
public class UsersServiceImpl implements BeanPropertiesUtil {
    @Value("${spring.profiles.active}")
    private String sysdev;

    UsersRepository getUsersRepository() {
        return usersRepository;
    }

    /**
     * 保存用户（道友）
     *
     * @param usersDO usersDO
     * @return UsersVO
     */
    public UsersVO save(UsersDO usersDO) {
        usersDO = usersRepository.save(usersDO);
        //设置一个不重复的默认账号，用于登录使用
        usersDO.setUsername("tongzhuo-" + usersDO.getId());
        if (Objects.nonNull(usersDO.getPhone())) {
            usersDO.setBingingPhone(YN.YES.getCode());
        }
        return UsersVO.from(usersRepository.save(usersDO));
    }

    /**
     * @param usersDO usersDO
     * @param state   如果不为空 则需要按照   邀请人,道友类型
     * @return UsersVO
     */
    @Transactional(rollbackFor = Exception.class)
    public UsersVO saveNonExistAddFriend(UsersDO usersDO, String state) {
        UsersVO usersVO = saveNotExist(usersDO);
        log.debug("usersDO = {} state = {}", usersDO, state);
        //添加道友 state为邀请者unionId
        if (StringUtils.isNotBlank(state) && Objects.nonNull(usersVO)) {
            addFriend(state, usersVO);
        }
        return usersVO;
    }

    @Transactional(rollbackFor = Exception.class)
    public UsersVO saveNotExist(UsersDO usersDO) {
        if (Objects.isNull(usersDO.getUnionId())) {
            throw new NullPointerException("usersDO.getUnionId() is null");
        }
        UsersDO exist = usersRepository.findByUnionId(usersDO.getUnionId());
        return Objects.isNull(exist) ? save(usersDO) : UsersVO.from(exist);
    }

    /**
     * 添加道友
     *
     * @param state   state
     * @param usersVO usersVO
     */
    private void addFriend(String state, UsersVO usersVO) {
        log.debug("邀请信息 state = {}", state);
        String[] strings = state.split(",");
        Long inviteUserId = Long.parseLong(strings[0]);
        Integer friendsType = 2;
        UsersVO inviteUser = find(inviteUserId);
        //邀请者不为空且不是自己时，添加道友
        if (Objects.nonNull(inviteUser) && !inviteUserId.equals(usersVO.getId())) {
            BusinessResult applyResult = tzUserFriendsService.applyBindFriends(inviteUser.getId(), usersVO.getId(), String.format("来自好友%s邀请", inviteUser.getName()), null, friendsType);
            log.debug("申请绑定道友结果 applyResult = {}", applyResult);
            //1代表同意
            BusinessResult bindResult = tzUserFriendsService.bindFriends(usersVO.getId(), inviteUser.getId(), 1);
            log.debug("道友绑定绑定结果 bindResult = {}", bindResult);
        }
    }

    /**
     * 根据id查询用户（道友）基本信息
     *
     * @param id 用户id
     * @return UsersVO
     */
    @Cacheable(cacheNames = USERS_BY_ID, key = "#id")
    public UsersVO find(Long id) {
        UsersVO usersVO = UsersVO.from(usersRepository.findOne(id));
        if (Objects.isNull(usersVO)) {
            return null;
        }
        log.debug("id={}, usersVO={}", id, usersVO);
        return usersVO;
    }

    /**
     * 根据id查询用户（道友）基本信息(无缓存)
     *
     * @param id 用户id
     * @return UsersVO
     */
    public UsersVO findOne(Long id) {
        Assert.notNull(id, "用户id不能为空！id = " + id);
        UsersVO usersVO = UsersVO.from(usersRepository.findOne(id));
        if (Objects.isNull(usersVO)) {
            return null;
        }
        log.debug("id={}, usersVO={}", id, usersVO);
        return usersVO;
    }


    public UsersVO findByUnionId(String unionId) {
        return UsersVO.from(usersRepository.findByUnionId(unionId));
    }


    /**
     * 根据name查询用户（道友）基本信息
     *
     * @param name 用户名称
     * @return List<UsersVO>
     */
    public List<UsersVO> findByName(String name) {
        List<UsersDO> userList = usersRepository.findByName(name);
        return userList.stream().map(UsersVO::from).collect(Collectors.toList());
    }

    /**
     * 根据phone查询用户（道友）基本信息
     *
     * @param phone 用户手机号
     * @return List<UsersVO>
     */
    public UsersVO findByPhone(String phone) {
        return UsersVO.from(usersRepository.findByPhone(phone));
    }

    /**
     * @param idList idList
     * @return List<UsersVO>
     */
    @Cacheable(cacheNames = USERS_IN_IDS, key = "#idList")
    public List<UsersVO> findByIdIn(List<Long> idList) {
        return usersRepository.findByIdInOrderByNameAsc(idList).stream().map(UsersVO::from).collect(Collectors.toList());
    }

    /**
     * @param idList idList
     * @return List<UsersVO>
     */
    public List<UsersVO> find(List<Long> idList) {
        return usersRepository.findByIdInOrderByNameAsc(idList).stream().map(UsersVO::from).collect(Collectors.toList());
    }

    public Page<UsersDO> findOnlineUsers(Pageable pageable) {
        Specification<UsersDO> specification = (root, query, cb) -> {
            List<Predicate> list = new ArrayList<>();
            list.add(cb.equal(root.get("online"), YN.YES.getCode()));
            return cb.and(list.toArray(new Predicate[0]));
        };
        return usersRepository.findAll(specification, pageable);
    }


    @Cacheable(cacheNames = CITY_LIST, key = "'all'", unless = "#result == null")
    public List<CityDO> cityList() {
        List<CityDO> cityDOS = cityRepository.findByLevel(2);
        List<CityDO> dos = cityDOS.stream().map(cityDO -> {
            List<CityDO> cityList = cityDO.getCityList();
            List<CityDO> doList = cityList.stream().map(cityDO1 -> {
                cityDO1.setCityList(null);
                return cityDO1;
            }).collect(Collectors.toList());
            cityDO.setCityList(doList);
            return cityDO;
        }).collect(Collectors.toList());
        return dos;
    }

    @Cacheable(cacheNames = SCHOOL_LIST, key = "'all'", unless = "#result == null")
    public List<SchoolDO> schoolList() {
        return schoolRepository.findAll();
    }

    public PageResultVO<String> findBySchoolNameLike(String name, Pageable pageable) {
        Page<SchoolDO> schoolDOPage;
        if (Objects.nonNull(name)) {
            schoolDOPage = schoolRepository.findBySchoolNameLikeOrderByTypeDescIdAsc("%" + name + "%", pageable);
        } else {
            schoolDOPage = schoolRepository.findAllByOrderByTypeDescIdAsc(pageable);
        }

        return new PageResultVO(schoolDOPage.getTotalElements(), schoolDOPage.getContent().stream().map(schoolDO -> schoolDO.getSchoolName()).collect(Collectors.toList()));
    }

    /**
     * CacheRemove自定义注解用于补充Caching的删除能力
     * 注册时更新使用（放置需要更新的字段即可，不更新的设置为空，如果目标存在的情况下 ，不会更新目标）
     *
     * @param editVO editVO
     * @return UsersVO
     */
    @Caching(evict = {
            @CacheEvict(cacheNames = USERS_BY_ID, key = "#editVO.id", condition = "#editVO.id != null"),
            @CacheEvict(cacheNames = USERS_BY_USERNAME, key = "#editVO.username", condition = "#editVO.username != null")
    })
    @Transactional(rollbackFor = Exception.class)
    public UsersDO update(UsersDO editVO) {

        UsersDO user = usersRepository.findOne(editVO.getId());
        log.info("更新用户信息 查询到用户：user = {}", user);
        Assert.notNull(user, "没有查询到对应的用户 editVO = " + editVO);

        if (Objects.nonNull(editVO.getAvatarUrl())
                && !editVO.getAvatarUrl().equals(user.getAvatarUrl())
                && LIVE_LEARNING.code.equals(editVO.getStatus())
                && Objects.nonNull(user.getRoomId())
        ) {
            gyfTaskExecutor.execute(() -> liveVideoService.changeRoomAvatar(user.getId(), user.getRoomId(), editVO.getAvatarUrl()));
        }

        if (Objects.nonNull(editVO.getPhone())) {
            editVO.setBingingPhone(YN.YES.getCode());
        }

        copyNonNullProperties(editVO, user);
        log.info("更新用户信息：user = {}", user);
        return usersRepository.save(user);
    }

    public void learnTimingHandler(Long userId) {
        log.info("旧版计时未结束，更新到新版的情况，userId {}", userId);
        //异步处理
        gyfTaskExecutor.execute(() -> {
            if (version.judgeVersion(userId, "3.3.0") >= 0) {
                log.info("大于等于3.3.0版本");
                UsersLearnTimingDO learnTimingDO = usersLearnTimingRepository.findByUserIdAndStatusOne(userId, 0);
                if (Objects.nonNull(learnTimingDO) && learnTimingDO.getType() == 1 && learnTimingDO.getStatus().intValue() == start.code.intValue() && Objects.isNull(learnTimingDO.getContentId())) {
                    //旧版计时未结束，更新到新版的情况
                    log.info("准备结束计时");
                    //为了兼容之前版本，清除缓存
                    String pauseKey = RedisKeyConst.getPauseLearnTimeByUserid(userId);
                    String continueKey = RedisKeyConst.getContinueLearnTimeByUserid(userId);
                    redisTemplate.delete(pauseKey);
                    redisTemplate.delete(continueKey);
//                    redisTemplate.opsForZSet().remove(RedisKeyConst.getUserSetLearnTime(), userId);
                    UsersVO usersVO = findOne(userId);
                    String key = RedisConfig.CacheKeys.USERS_BY_USERNAME + ":" + usersVO.getUsername();
                    log.debug("tongzhuo:users:by-username={}", key);
                    redisTemplate.delete(key);
                    redisTemplate.delete(USERS_BY_ID + ":" + userId);

                    //更新各个学习状态
                    updateStatus(userId, NO_TIMING.getCode(), null, null, null);
                    learnTimingDO.setStatus(UsersLearnTimingDO.StatusEnum.valid.code);
                    usersLearnTimingRepository.save(learnTimingDO);
                }
            }
        });
    }

    /**
     * 内部使用，更新用户目标
     *
     * @return UsersVO
     */
    @Caching(evict = {
            @CacheEvict(cacheNames = USERS_BY_ID, key = "#id")
    })
    @Transactional(rollbackFor = Exception.class)
    public UsersVO updateCurrentLearnTarget(Long id, Long currentLearnTargetId, String currentLearnTargetName) {
        log.info("更新用户信息:学习目标 id={}", id);
        UsersDO usersDO = usersRepository.findOne(id);
        if (Objects.isNull(usersDO)) {
            throw new NullPointerException("usersDO is null");
        }
        usersDO.setCurrentLearnTargetId(currentLearnTargetId);
        usersDO.setCurrentLearnTargetName(currentLearnTargetName);
        return UsersVO.from(usersRepository.save(usersDO));
    }

    /**
     * 更改用户学习状态
     *
     * @return UsersVO
     */
    @Caching(evict = {
            @CacheEvict(cacheNames = USERS_BY_ID, key = "#id")
    })
    @Transactional(rollbackFor = Exception.class)
    @Modifying(clearAutomatically = true)
    public UsersVO updateStatus(Long id, Integer status, Date startDateTime, Long roomId, Long lianMaiId) {
        UsersDO usersDO = usersRepository.findOne(id);
        if (Objects.isNull(usersDO)) {
            sendExceptionUtil.sendException("查询用户信息失败 userid=" + id);
            throw new NullPointerException("usersDO is null");
        }
        redisTemplate.delete(USERS_BY_USERNAME + ":" + usersDO.getUsername());
        redisTemplate.delete(USERS_BY_ID + ":" + id);

        usersDO.setStatus(status);
        if (LIVE_LEARNING.code.equals(status) || LEARN_TIME_ROOM_LEARNING.code.equals(status)) {
            usersDO.setLianMaiId(lianMaiId);
            usersDO.setRoomId(roomId);
        } else {
            usersDO.setLianMaiId(null);
            usersDO.setRoomId(null);
        }
        if (LearningStatus.ifLearningStart(status)) {
            if (null == startDateTime) {
                usersDO.setStartLearnTime(new Date());
            } else {
                usersDO.setStartLearnTime(startDateTime);
            }
        } else if (LearningStatus.getLearningStatus(status) == LearningStatus.NO_TIMING) {
            if (null == usersDO.getStartLearnTime()) {
                throw new CheckParamException("user is already stop learning, please don't repeat stop");
            }
            usersDO.setStartLearnTime(null);
        }
        UsersDO save = usersRepository.saveAndFlush(usersDO);
        return UsersVO.from(save);
    }

    @Transactional
    public void updateVipStatus(List<Long> ids, Integer vipStatus) {
        if(Objects.isNull(ids) || ids.isEmpty()){
            return;
        }
        usersRepository.updateVipStatus(ids, vipStatus);
    }

    public boolean sendSMSWithKong(String app, String phoneNumber, String reportContent, ArrayList<String> params) {
        checkSendFrequency(phoneNumber);

        String code = StringUtil.genRandom(4);
        JSONObject resJson;
        //发送模板消息短信-集团短信接口
        String res = "";
        if (StringUtil.isEmpty(reportContent)) {
//              res = kongClientUtil.sendMMCheckCode(phoneNumber, code);
            UsersVO usersVO = findByPhone(phoneNumber);
            if (Objects.nonNull(usersVO) && LOGIN_LIMIT_STATUS.equals(usersVO.getStatus())) {
                throw new UserLoginLimitException();
            }
            try {
//                res = kongClientUtil.sendTplReport(app, phoneNumber, "登录验证码：" + code + "（120秒有效）。请勿泄露！");
                res = kongClientUtil.sendMMCheckCode(app, phoneNumber, code);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            try {
                res = kongClientUtil.sendTplReport(app, phoneNumber, reportContent);
            } catch (IOException e) {
                e.printStackTrace();
            }
            resJson = JSONObject.parseObject(res);
            if (resJson.getString("code") != null && "0000".equals(resJson.getString("code"))) {
                //请求成功
                log.info("sendReportSMSResponse  response={},phoneNo={},", resJson, phoneNumber);
                return true;
            } else if (params != null && params.size() > 0) {
                String result = tencentSms.sendReport(phoneNumber, params);
                if ("0".equals(result)) {
                    return true;
                } else {
                    log.info("腾讯云发送短信失败");
                }
            }
            return false;
        }
        resJson = JSONObject.parseObject(res);

        //将验证码对应手机号存入redis中以便验证
        String phoneVerifyCodeKey = RedisKeyConst.getPhoneVerifyCode(phoneNumber);
        redisTemplate.opsForValue().set(phoneVerifyCodeKey, code, ExpireTime.FIVE_MIN.getTime(), TimeUnit.MINUTES);
        //db插入验证码数据
        SmsMobileIdentifyCode smsMobileIdentifyCode = new SmsMobileIdentifyCode();
        smsMobileIdentifyCode.setStatus(GlobalStatus.VALID.getCode());
        smsMobileIdentifyCode.setCreateTime(new Date());
        smsMobileIdentifyCode.setPhone(phoneNumber);
        smsMobileIdentifyCode.setIdentifyCode(code + "");
        Date fiveMinLater = new Date(System.currentTimeMillis() + (1000 * 60 * 5));
        smsMobileIdentifyCode.setExpireTime(fiveMinLater);
        smsMobileIdentifyCodeRepository.save(smsMobileIdentifyCode);
        //验证
        if (resJson.getString("code") != null && "0000".equals(resJson.getString("code"))) {
            //请求成功
            log.info("sendSMSResponse  response={},phoneNo={},", resJson, phoneNumber);
            return true;
        } else {

            if (app.equals(AppEnum.postgraduate.name())) {
                return false;
            }

            ArrayList<String> codeParams = new ArrayList<>();
            codeParams.add(code);
            codeParams.add("2");
            String result = tencentSms.sendVerifyCode(phoneNumber, codeParams);
            if ("0".equals(result)) {
                return true;
            } else {
                log.info("腾讯云发送短信失败");
            }
        }
        log.info("sendSMSResponse response={},phoneNo={},", resJson, phoneNumber);
        return false;

    }

    public boolean verifySMSWithKong(String phone, String code) {
        checkVerifyFrequency(phone);
        String redisKey = RedisKeyConst.getPhoneVerifyCode(phone);
        String redisValue = (String) redisTemplate.opsForValue().get(redisKey);
        if (null != redisValue && redisValue.equals(code)) {
            return true;
        }
        String dateString = CalendarUtil.getDateString(new Date(), "yyyy-MM-dd HH:mm:ss");
        List<SmsMobileIdentifyCode> smsMobileIdentifyCodeList = smsMobileIdentifyCodeRepository.findByPhoneNoAndStateAndIdentifyCodeAndExpireTimeAfter
                (phone, GlobalStatus.VALID.getCode(), code, dateString);
        if (null != smsMobileIdentifyCodeList && smsMobileIdentifyCodeList.size() > 0) {
            smsMobileIdentifyCodeRepository.updateState(smsMobileIdentifyCodeList.get(0).getId(), GlobalStatus.INVALID.getCode());
            return true;
        }
        return false;
    }

    private void checkSendFrequency(String phoneNumber) {
        String key = RedisKeyConst.getSendPhoneCodeFrequency(phoneNumber);
        if (redisTemplate.opsForValue().get(key) != null) {
            throw new CheckRunException("发送验证码操作过于频繁，请稍等一会儿再提交！");
        } else {
            redisTemplate.opsForValue().set(key, 1, ExpireTime.FIVE_SEC.getTime(), TimeUnit.SECONDS);
        }
    }

    private void checkVerifyFrequency(String phoneNumber) {
        String key = RedisKeyConst.getVerifyPhoneCodeFrequency(phoneNumber);
        if (redisTemplate.opsForValue().get(key) != null) {
            throw new CheckRunException("校验验证码操作过于频繁，请稍等一会儿再提交！");
        } else {
            redisTemplate.opsForValue().set(key, 1, ExpireTime.ONE_SEC.getTime(), TimeUnit.SECONDS);
        }
    }

    public List<UsersGroupVO> userGroupList(Long userId, Integer toUserId) {
        JSONObject jsonObject = usersGroupClient.getGroupInfoListByUserId(userId, toUserId);
        log.info("调用群组系统查询用户当前群组信息列表 jsonObject={}", jsonObject);
        if (null == jsonObject || null == jsonObject.getInteger("code") || 0 != jsonObject.getInteger("code")) {
            throw new CheckRunException("调用群组系统查询用户当前群组信息列表异常");
        }
        JSONArray groupUserArray = jsonObject.getJSONArray("data");
        if (null == groupUserArray || groupUserArray.isEmpty()) {
            return null;
        }
        return JSONObject.parseArray(groupUserArray.toJSONString(), UsersGroupVO.class);
    }

    @Cacheable(cacheNames = USERS_EXT_BY_USER_Id, key = "#userId")
    public UsersExtInfoDO findExtInfoByUserId(Long userId) {
        return usersExtInfoRepository.findByUserId(userId);
    }


    @Async
    @Caching(evict = {
            @CacheEvict(cacheNames = USERS_EXT_BY_USER_Id, key = "#usersExtInfoDO.userId", condition = "#usersExtInfoDO.userId != null")
    })
    @Transactional
    public UsersExtInfoDO saveExtInfo(UsersExtInfoDO usersExtInfoDO) {
        Assert.notNull(usersExtInfoDO.getUserId(), "userId不能为空！");
        UsersExtInfoDO usersExtInfo = findExtInfoByUserId(usersExtInfoDO.getUserId());
        if (Objects.isNull(usersExtInfo)) {
            return usersExtInfoRepository.save(usersExtInfoDO);
        }
        copyNonNullProperties(usersExtInfoDO, usersExtInfo);
        return usersExtInfoRepository.save(usersExtInfo);
    }


    /**
     * 根据name查询用户（道友）基本信息
     *
     * @param type 用户类型
     * @return List<UsersVO>
     */
    public List<UsersVO> findByType(Integer type) {
        List<UsersDO> userList = usersRepository.findByType(type);
        return userList.stream().map(UsersVO::from).collect(Collectors.toList());
    }

    /**
     * 用户扩展信息分页
     *
     * @param pageable pageable
     * @return Page
     */
    public Page<UsersExtInfoDO> userExtPage(Pageable pageable) {
        return usersExtInfoRepository.findAll(pageable);
    }

    public PageResultVO<UsersVO> findByNameLike(String name, Long userId, Pageable pageable) {
        Page<UsersDO> usersDOPage = usersRepository.findAllByNameLikeAndIdIsNot("%" + name + "%", userId, pageable);
        List<UsersVO> usersVOList = usersDOPage.getContent().stream().map(UsersVO::from).collect(Collectors.toList());
        return new PageResultVO(usersDOPage.getTotalElements(), usersVOList);
    }

    public Integer cacheWechatLogin() {
        String key = RedisKeyConst.ifWechatLogin("user-center");
        Object o = redisTemplate.opsForValue().get(key);
        if (null == o) {
            redisTemplate.opsForValue().set(key, 1);
            return 1;
        }
        return (Integer) o;
    }

    public Integer groupSee() {
        String key = RedisKeyConst.ifGroupSee("user-center");
        Object o = redisTemplate.opsForValue().get(key);
        if (null == o) {
            redisTemplate.opsForValue().set(key, 1);
            return 1;
        }
        return (Integer) o;
    }

    @Cacheable(cacheNames = USERS_BLACK, key = "#userId")
    public List<TzUserBlacklist> blacklist(Long userId) {
        return tzUserFriendsService.myBlacklist(userId).getData();
    }


    public Integer maxUserId() {
        return usersRepository.maxUserId();
    }


    public Page<UsersDO> findAllLiveUsers(Pageable pageable) {
        //获取一小个小时前数据
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.set(Calendar.MINUTE,
                calendar.get(Calendar.MINUTE) - minute);

        Specification<UsersDO> specification = (root, query, cb) -> {
            List<Predicate> list = new ArrayList<>();
            list.add(cb.equal(root.get("status"), LIVE_LEARNING.code));
            list.add(cb.greaterThanOrEqualTo(root.get("startLearnTime"), calendar.getTime()));
            return cb.and(list.toArray(new Predicate[list.size()]));
        };
        return usersRepository.findAll(specification, pageable);
    }

    public List<UsersDO> findOffLineUsers(List<Long> userIdList) {
        if (Objects.isNull(userIdList) || userIdList.isEmpty()) {
            return new ArrayList<>();
        }
        Specification<UsersDO> specification = (root, query, cb) -> {

            Path<Object> path = root.get(FIELD_ID);
            CriteriaBuilder.In<Object> in = cb.in(path);
            userIdList.forEach(in::value);
            List<Predicate> list = new ArrayList<>();
            list.add(cb.equal(root.get(FIELD_ONLINE), YN.NO.getCode()));
            list.add(in);
            return cb.and(list.toArray(new Predicate[0]));
        };
        return usersRepository.findAll(specification);
    }

    @Async
    public void saveUserActiveLog(Long userId, String userName) {
        if (Objects.isNull(userId)) {
            return;
        }
        try {
            Boolean exists = logService.findByUserId(DateUtil.getStringDateShort(), userId);
            if (Objects.isNull(exists) || !exists) {
                UserActiveLogDO userActiveLogDO = UserActiveLogDO.builder().date(DateUtil.getStringDateShort()).userId(userId).userName(userName).build();
                logService.save(userActiveLogDO);
            }
        } catch (Exception e) {
            log.error("", e);
        }
    }


    @Transactional
    public Integer updateOnlineById(Long userId, Integer online, Integer topRun, Long channelId, Long backRunTime) {
        UsersDO usersDO = usersRepository.findOne(userId);
        if (Objects.isNull(usersDO)) {
            return 1;
        }
        Assert.notNull(userId, "userId is null !");
        Assert.notNull(online, "online is null !");
        Assert.notNull(channelId, "channelId is null !");
        if (online.equals(YN.YES.getCode())) {
            topRun = Objects.isNull(topRun) ? YN.YES.getCode() : topRun;
        } else {
            topRun = null;
        }
        //异步处理
        Integer finalTopRun = topRun;
        Long finalBackRunTime = backRunTime;
        commonTaskExecutor.execute(() -> {
            //后台运行
            if (Objects.nonNull(finalTopRun)
                    && YN.NO.getCode().equals(finalTopRun)
                    && Objects.nonNull(finalBackRunTime)
                    && finalBackRunTime > 0) {
                log.info("用户{}后台运行，30秒后触发", userId);
                scheduledExecutorService.schedule(() -> learnRoomPush(userId), DateUtil.getDelayDate(30, TimeUnit.SECONDS));
            }
        });

        if (Objects.nonNull(backRunTime)
                && YN.NO.getCode().equals(backRunTime)
                && YN.NO.getCode().equals(online)
                && Objects.nonNull(usersDO.getBackRunTime())) {
            backRunTime = usersDO.getBackRunTime().getTime();
        }
        Integer existOnlineState = usersDO.getOnline();
        Integer index = usersRepository.updateOnlineById(userId, online, topRun, channelId, DateUtil.dateToStrLong(new Date(backRunTime)));
        log.info("index = {} online = {} usersDO = {}", index, online, usersDO);
        //上线、下线需要处理的逻辑
        if (!online.equals(existOnlineState) && index > 0) {
            if (online.equals(YN.YES.getCode())) {
                commonTaskExecutor.execute(() -> onlineInitializerChain.init(userId));
            } else {
                commonTaskExecutor.execute(() -> offlineInitializerChain.init(userId));
            }
        }
        return index;
    }


    public void learnRoomPush(Long userId) {

        //确认是否还在后台运行
        UsersDO usersDO = usersRepository.findOne(userId);
        if (Objects.nonNull(usersDO.getBackRunTime()) && (Objects.isNull(usersDO.getTopRun()) || usersDO.getTopRun() != YN.YES.getCode()) && (System.currentTimeMillis() - usersDO.getBackRunTime().getTime()) / 1000 >= 30) {
            log.info("用户{}满足条件，后台运行时长{}秒", userId, (System.currentTimeMillis() - usersDO.getBackRunTime().getTime()) / 1000);
            UsersLearnTimingDO learnTimingDO = usersLearnTimingRepository.findByUserIdAndStatusOne(userId, 0);
            log.info("计时信息  {}", learnTimingDO);
            //自习室学习中
            if (Objects.nonNull(learnTimingDO) && learnTimingDO.getType().intValue() == TIME_LEARN_ROOMING.code.intValue()) {
                //专注时间是否使用其他app
                BusinessResult<TimeLearnRoomSeatsVO> seatsVO = liveVideoService.detailUser(userId);
                log.info("服务返回  {}", seatsVO);
                TimeLearnRoomSeatsVO seats = seatsVO.getData();
                log.info("自习室座位信息  {}", seats);
                if (Objects.nonNull(seats) && YN.NO.getCode().equals(seats.getUseOtherApps())) {
                    log.info("专注时间不使用其他app");
                    String content = "当前正在同桌计时，请保持专注！";
                    if (sysdev.equals("test")) {
                        content = DateUtil.dateToStrHMS(new Date()) + content;
                    }
                    PushDTO pushDTO = PushDTO.builder().content(content).type(1).ids(Lists.newArrayList(userId + "")).build();
                    deskMatePushService.push(pushDTO);
                    scheduledExecutorService.schedule(() -> learnRoomPush(userId), DateUtil.getDelayDate(30, TimeUnit.SECONDS));
                }

            }
        }

    }

    public List<UsersVO> findOnlineFriend(Long userId) {

        List<Long> userIdList = userFriendsService.followMe(userId).getData();
        List<Long> friendIdList = userFriendsService.friendsAll(userId).getData()
                .stream()
                .map(UserFriendInfo::getFriendsUserId)
                .collect(Collectors.toList());
        userIdList.addAll(friendIdList);
        userIdList = userIdList.stream().distinct().collect(Collectors.toList());

        if (userIdList.isEmpty()) {
            return new ArrayList<>();
        }

        return usersRepository.findByIdInAndOnline(userIdList, YN.YES.getCode())
                .stream()
                .map(UsersVO::from)
                .collect(Collectors.toList());
    }


    private static final Integer minute = 60 * 10;
    @Lazy
    @Autowired
    private RedisTemplate redisTemplate;
    @Lazy
    @Autowired
    private KongClientUtil kongClientUtil;
    @Lazy
    @Autowired
    private TencentSms tencentSms;
    @Lazy
    @Autowired
    private SmsMobileIdentifyCodeRepository smsMobileIdentifyCodeRepository;
    @Lazy
    @Autowired
    private UsersRepository usersRepository;
    @Lazy
    @Autowired
    private GroupInfoService usersGroupClient;
    @Lazy
    @Autowired
    private UserFriendsService tzUserFriendsService;
    @Lazy
    @Autowired
    private LogServiceImpl logService;
    @Autowired
    protected SendExceptionUtil sendExceptionUtil;
    @Autowired
    private LiveVideoService liveVideoService;
    @Autowired
    private CityRepository cityRepository;
    @Autowired
    private SchoolRepository schoolRepository;
    @Lazy
    @Autowired
    Version version;
    @Autowired
    private UsersLearnTimingRepository usersLearnTimingRepository;
    @Resource(name = "gyfTaskExecutor")
    AsyncTaskExecutor gyfTaskExecutor;
    @Autowired
    private DeskMatePushService deskMatePushService;
    @Lazy
    @Autowired
    private UsersExtInfoRepository usersExtInfoRepository;
    @Lazy
    @Autowired
    private OfflineProcessorChain offlineInitializerChain;
    @Lazy
    @Autowired
    private OnlineProcessorChain onlineInitializerChain;
    @Resource(name = "scheduledExecutorService")
    private ThreadPoolTaskScheduler scheduledExecutorService;
    @Autowired
    private UserFriendsService userFriendsService;
    @Resource(name = "commonTaskExecutor")
    private AsyncTaskExecutor commonTaskExecutor;


}

